package digo

import (
	"fmt"
	"reflect"
	"strconv"
	"unicode"
	"unicode/utf8"
)

var valueCache = make(map[reflect.Type]reflect.Value)

func Value(def interface{}) interface{} {
	return value(reflect.ValueOf(def).Type()).Interface()
}

func value(defType reflect.Type) reflect.Value {
	def := elemType(defType)
	var target reflect.Value
	if def.Field(0).Tag.Get("singleton") == "true" {
		if target, ok := valueCache[def]; !ok {
			target = allocValue(def)
			valueCache[def] = target
		}
	} else { target = allocValue(def) }
	if defType.Kind() == reflect.Ptr { return target.Addr() }
	return target
}

func allocValue(defType reflect.Type) reflect.Value {
	var target reflect.Value
	fldIdx := 1
	if defType.Field(0).Tag.Get("alloc") == "factory" {
		target, fldIdx = allocValueByFactory(defType)
	} else {
		targetType := elemType(defType.Field(0).Type)
		target = reflect.Indirect(reflect.New(targetType))
	}
	for i := fldIdx; i < defType.NumField(); i++ {
		field := defType.Field(i)
		var val reflect.Value
		if str := field.Tag.Get("value"); str != "" {
			val = valueToSet(str, field.Type)
		} else {
			val = value(field.Type)
			if name := field.Tag.Get("prop"); name != "" {
				val = propValue(val, name)
			}
		}
		
		if prop := target.Addr().MethodByName(setterName(field.Name))
		   prop.IsValid() {
		   	prop.Call([]reflect.Value{val})
		} else if prop = target.FieldByName(field.Name); prop.IsValid() {
			prop.Set(val)
		} else {
			panic(fmt.Sprintf("digo:%s:no property `%s'",
							  target.Type().Name(), field.Name))
		}
	}
	return target
}

func allocValueByFactory(defType reflect.Type) (reflect.Value, int) {
	methodName := defType.Field(1).Tag.Get("method")
	if methodName == "" {
		panic(fmt.Sprintf("digo:%s:factory method not specified",
						  defType.Field(0).Name))
	}
	params := []reflect.Value{}
	var idx int
	for idx = 2; ; idx++ {
		field := defType.Field(idx)
		var val reflect.Value
		if tagval := field.Tag.Get("paramValue"); tagval != "" {
			val = valueToSet(tagval, field.Type)
		} else if field.Tag == "param" || field.Tag.Get("param") != "" {
			val = value(field.Type)
		} else if tagval = field.Tag.Get("paramProp"); tagval != "" {
			val = propValue(value(field.Type), tagval)
		} else { break }
		params = append(params, val)
	}
	factory := reflect.New(elemType(defType.Field(1).Type))
	if method := factory.MethodByName(methodName); method.IsValid() {
		return elemVal(method.Call(params)[0]), idx
	}
	if method := factory.Elem().MethodByName(methodName); method.IsValid() {
		return elemVal(method.Call(params)[0]), idx
	}
	panic(fmt.Sprintf("digo:%s:factory method %s not found",
					  factory.Type().Name(), methodName))
}

func elemVal(val reflect.Value) reflect.Value {
	if val.Kind() == reflect.Ptr { return val.Elem() }
	return val
}

func valueToSet(tagval string, typ reflect.Type) reflect.Value {
	if tagval == "" { return reflect.ValueOf(nil) }
	switch typ.Kind() {
	case reflect.String:
		return reflect.ValueOf(tagval)
	case reflect.Int:
		return reflect.ValueOf(int(parseInt(tagval, 0)))
	case reflect.Int8:
		return reflect.ValueOf(int8(parseInt(tagval, 8)))
	case reflect.Int16:
		return reflect.ValueOf(int16(parseInt(tagval, 16)))
	case reflect.Int32:
		return reflect.ValueOf(int32(parseInt(tagval, 32)))
	case reflect.Int64:
		return reflect.ValueOf(int64(parseInt(tagval, 64)))
	case reflect.Float32:
		return reflect.ValueOf(float32(parseFloat(tagval, 32)))
	case reflect.Float64:
		return reflect.ValueOf(float64(parseFloat(tagval, 64)))
	default:
		panic(fmt.Sprintf("digo:%s:unsupported type", typ.Name()))
	}
}

func parseInt(str string, bitSize int) int64 {
	i, e := strconv.ParseInt(str, 0, bitSize)
	if e != nil { panic("digo:" + e.Error()) }
	return i
}

func parseFloat(str string, bitSize int) float64 {
	f, e := strconv.ParseFloat(str, bitSize)
	if e != nil { panic("digo:" + e.Error()) }
	return f
}

func propValue(ref reflect.Value, propName string) reflect.Value {
	ptr, elem := ptrAndElemValue(ref)
	if prop := ptr.MethodByName(getterName(propName)); prop.IsValid() {
		return prop.Call([]reflect.Value{})[0]
	}
	if prop := elem.MethodByName(getterName(propName)); prop.IsValid() {
		return prop.Call([]reflect.Value{})[0]
	}
	if prop := elem.FieldByName(propName); prop.IsValid() {
		return prop
	}
	panic(fmt.Sprintf("digo:%s:no property `%s'",
	                  elem.Type().Name(), propName))
}

func elemType(typ reflect.Type) reflect.Type {
	if typ.Kind() == reflect.Ptr { return typ.Elem() }
	return typ
}

func ptrAndElemValue(val reflect.Value) (ptr reflect.Value, elem reflect.Value) {
	if val.Kind() == reflect.Ptr {
		ptr = val
		elem = val.Elem()
	} else {
		ptr = val.Addr()
		elem = val
	}
	return
}

func setterName(propName string) string {
	return "Set" + capitalize(propName)
}

func getterName(propName string) string {
	return capitalize(propName)
}

func capitalize(propName string) string {
	if ch, sz := utf8.DecodeRuneInString(propName); unicode.IsLower(ch) {
		return string(unicode.ToUpper(ch)) + propName[sz:]
	}
	return propName
}
		